package com.hk.core.data.jpa.query.specification;

import com.hk.commons.query.MatchMode;
import com.hk.commons.util.ArrayUtils;
import com.hk.commons.util.StringUtils;
import jakarta.annotation.Nullable;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.Specification;

import java.util.Collection;
import java.util.List;


/**
 * @author Kevin
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Specifications {

    public static <T> Specification<T> equals(String property, Object value) {
        return (root, query, cb) -> cb.equal(root.get(property), value);
    }

    /**
     * <pre>
     *  xx = ?
     * </pre>
     */
    @Nullable
    public static <T> Specification<T> equals(boolean matcher, String property, Object value) {
        return matcher ? (root, query, cb) -> cb.equal(root.get(property), value) : null;
    }

    /**
     * <pre>
     * xx <> ?
     * </pre>
     */
    public static <T> Specification<T> notEqual(String property, Object value) {
        return (root, query, cb) -> cb.notEqual(root.get(property), value);
    }

    @Nullable
    public static <T> Specification<T> notEqual(boolean matcher, String property, Object value) {
        return matcher ? (root, query, cb) -> cb.notEqual(root.get(property), value) : null;
    }

    /**
     * <pre>
     * (xx IS NULL)
     * </pre>
     */
    public static <T> Specification<T> isNull(String property) {
        return (root, query, cb) -> cb.isNull(root.get(property));
    }

    /**
     * <pre>
     * (xx IS NULL OR xx  = '')
     * </pre>
     */
    public static <T> Specification<T> isNullOrEmpty(String property) {
        return (root, query, cb) -> {
            Path<?> path = root.get(property);
            List<Predicate> predicates = ArrayUtils.asArrayList(cb.isNull(path), cb.equal(path, StringUtils.EMPTY));
            return cb.or(predicates.toArray(new Predicate[0]));
        };
    }

    /**
     * <pre>
     * (xx IS NOT NULL)
     * </pre>
     */
    public static <T> Specification<T> isNotNull(String property) {
        return (root, query, cb) -> cb.isNotNull(root.get(property));
    }

    /**
     * <pre>
     * (xx LIKE ?)
     * </pre>
     */
    public static <T> Specification<T> like(String property, String value, MatchMode matchMode) {
        return (root, query, cb) -> cb.like(root.get(property), matchMode.toMatchString(value));
    }

    /**
     * <pre>
     * (xx LIKE ?)
     * </pre>
     */
    @Nullable
    public static <T> Specification<T> like(boolean matcher, String property, String value, MatchMode matchMode) {
        return matcher ? (root, query, cb) -> cb.like(root.get(property), matchMode.toMatchString(value)) : null;
    }

    /**
     * <pre>
     *  property > ?
     * </pre>
     */
    public static <T extends Number, E> Specification<E> gt(String property, T value) {
        return (root, query, cb) -> cb.gt(root.get(property), value);
    }

    public static <T extends Comparable<? super T>> Specification<T> greaterThan(String property, T value) {
        return (root, query, cb) -> cb.greaterThan(root.get(property), value);
    }

    /**
     * <pre>
     *     property >= ?
     * </pre>
     */
    public static <T extends Comparable<? super T>, E> Specification<E> gte(String property, T value) {
        return (root, query, cb) -> cb.greaterThanOrEqualTo(root.get(property), value);
    }

    /**
     * <pre>
     *     property < ?
     * </pre>
     */
    public static <T extends Number, E> Specification<E> lt(String property, T value) {
        return (root, query, cb) -> cb.lt(root.get(property), value);
    }

    /**
     * <pre>
     *  property < ?
     * </pre>
     */
    public static <T extends Comparable<? super T>, E> Specification<E> lessThan(String property, T value) {
        return (root, query, cb) -> cb.lessThan(root.get(property), value);
    }

    /**
     * <pre>
     *     property <= ?
     * </pre>
     */
    public static <T extends Comparable<? super T>, E> Specification<E> lte(String property, T value) {
        return (root, query, cb) -> cb.lessThanOrEqualTo(root.get(property), value);
    }

    public static <T> Specification<T> like(String property, String value) {
        return (root, query, cb) -> cb.like(root.get(property), MatchMode.ANYWHERE.toMatchString(value));
    }

    @Nullable
    public static <T> Specification<T> like(boolean matcher, String property, String value) {
        return matcher ? (root, query, cb) -> cb.like(root.get(property), MatchMode.ANYWHERE.toMatchString(value)) : null;
    }

    public static <T> Specification<T> notNike(String property, String value) {
        return (root, query, cb) -> cb.notLike(root.get(property), MatchMode.ANYWHERE.toMatchString(value));
    }

    @Nullable
    public static <T> Specification<T> notNike(boolean matcher, String property, String value) {
        return matcher ? (root, query, cb) -> cb.notLike(root.get(property), MatchMode.ANYWHERE.toMatchString(value)) : null;
    }

    /**
     * <pre>
     *  property between ? and ?
     * </pre>
     */
    public static <T extends Comparable<? super T>, E> Specification<E> between(String property, T start, T end) {
        return (root, query, cb) -> cb.between(root.get(property), start, end);
    }

    /**
     * <pre>
     *  property IN (?,?)
     * </pre>
     */
    public static <T> Specification<T> in(String property, Collection<?> coll) {
        return (root, query, cb) -> cb.in(root.get(property)).value(coll);
    }

    /**
     * <pre>
     *  property IN (?,?)
     * </pre>
     */
    public static <T> Specification<T> in(String property, Object... value) {
        return (root, query, cb) -> cb.in(root.get(property)).value(value);
    }

    /**
     * <pre>
     *     property not in (?,?)
     * </pre>
     */
    public static <T> Specification<T> notIn(String property, Collection<?> coll) {
        return (root, query, cb) -> cb.in(root.get(property)).value(coll).not();
    }

    /**
     * <pre>
     *     property not in (?,?)
     * </pre>
     */
    public static <T> Specification<T> notIn(String property, Object... value) {
        return (root, query, cb) -> cb.in(root.get(property)).value(value).not();
    }


}
